/*
 * Project: Set Sample
 * Copyright (C) 2013 alf.labs gmail com,
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.alflabs.setsample.ui;

import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.*;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.util.Log;
import android.view.*;
import android.widget.Button;
import android.widget.Checkable;
import android.widget.TextView;
import android.widget.Toast;
import com.alflabs.annotations.NonNull;
import com.alflabs.annotations.Null;
import com.alflabs.setsample.BuildConfig;
import com.alflabs.setsample.R;
import com.alflabs.setsample.stats.SetResult;
import com.alflabs.setsample.presenter.IAbstractView;
import com.alflabs.setsample.model.SetBoard;
import com.alflabs.setsample.model.SetCard;
import com.alflabs.setsample.model.SetModel;
import com.alflabs.setsample.model.SetSolution;
import com.alflabs.setsample.presenter.SetPresenter;
import com.alflabs.setsample.stats.GAEvent;
import com.alflabs.setsample.stats.GAHelper;
import com.alflabs.setsample.widgets.SetCheckButton;

import java.util.Random;
import java.util.Set;

public class SetBoardFragment
        extends Fragment
        implements IAbstractView, SetCompletedDialog.ICompletedDialogCallback {

    private static final boolean DEBUG = BuildConfig.DEBUG;
    private static final String TAG = SetBoardFragment.class.getSimpleName();

    private static final int MSG_UPDATE_TIME_COUNTER = 42;
    private static final int MSG_UNPAUSE_TIME_COUNTER = MSG_UPDATE_TIME_COUNTER + 1;

    /** Presenter is null before onViewCreated(). Should not be null after. */
    private @Null
    SetPresenter mPresenter;

    private SetCheckButton mButtons[] = new SetCheckButton[SetBoard.SXY];
    private StringBuilder mTimeSB = new StringBuilder();   // for updateTime()
    private Handler mTimeHandler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message message) {
            return SetBoardFragment.this.handleMessage(message);
        }
    });

    private boolean mTimeIsPaused = true;
    private boolean mTimeWaitForWindowFocus = false;
    private long    mTimeLastUpdateMs = 0;
    private TextView mTimeCounterView;

    @Override
    public void onCreate(Bundle state) {
        super.onCreate(state);

        // Tell the framework to try to keep this fragment around during a configuration change.
        setRetainInstance(true);
        // Indicate we'll contribute items to the action bar
        setHasOptionsMenu(true);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle state) {
        View root = inflater.inflate(R.layout.set_board_fragment, container, false);
        initUi(root);
        return root;
    }

    public void setTimeCounterView(TextView timeCounterView) {
        if (DEBUG) Log.d(TAG, "setTimeCounterView attach: " + timeCounterView);                     //NLS
        mTimeCounterView = timeCounterView;
    }

    private void initUi(View root) {
        View.OnClickListener cl = new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                onButtonChecked(v);
            }
        };
        for (int k = 0; k < SetBoard.SXY; k++) {
            //noinspection SpellCheckingInspection
            String name = "com.alflabs.setsample:id/button" + (1+k);                                //NLS
            int id = getResources().getIdentifier(name, null, null);
            Button b = mButtons[k] = (SetCheckButton) root.findViewById(id);
            b.setTag(R.id.cell_index, k);
            b.setOnClickListener(cl);
        }
    }

    @Override
    public void onViewCreated(View view, Bundle state) {
        super.onViewCreated(view, state);

        if (mPresenter == null) {
            mPresenter = new SetPresenter(this);
        }
        if (state != null) {
            mPresenter.restoreState(state);
        }

        SetModel m = mPresenter.getModel();
        if (m.getSeed() == 0 || m.isReadyForNewGame()) {
            m.randomizeNewGame(new Random());
        }

        m.getStats().beginLoadAsync(getActivity());

        mPresenter.connectView(this);
        resetBoard();
        resetSolutions();
    }

    @Override
    public void onDetach() {
        super.onDetach();
        mTimeCounterView = null;
        if (DEBUG) Log.d(TAG, "setTimeCounterView detatch: null");                                  //NLS
    }

    @Override
    public void onResume() {
        super.onResume();
        startTimeCounter();
    }

    @Override
    public void onPause() {
        super.onPause();
        stopTimeCounter();
    }

    @SuppressWarnings("NullableProblems")
    @Override
    public void onSaveInstanceState(Bundle bundle) {
        super.onSaveInstanceState(bundle);
        if (mPresenter != null) {
            mPresenter.saveState(bundle);
        }
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        inflater.inflate(R.menu.set_activity, menu);
        super.onCreateOptionsMenu(menu, inflater);

        MenuItem i = menu.findItem(R.id.menu__action_show_solutions);
        if (i != null) {
            i.setCheckable(true);
        }

        if (DEBUG) {
            menu.add(0 /*groupId*/, 42 /*itemId*/, 0 /*order*/, "Solve now (debug)");               //NLS
        }
    }

    @Override
    public void onPrepareOptionsMenu(Menu menu) {
        super.onPrepareOptionsMenu(menu);
        MenuItem i = menu.findItem(R.id.menu__action_show_solutions);
        if (i != null) {
            i.setChecked(mPresenter != null && mPresenter.isDisplayAllSolutions());
        }
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
        case R.id.menu__action_new_board:
            onOptionsNewBoard();
            break;

        case R.id.menu__action_help:
            Intent i = new Intent(getActivity(), HelpActivity.class);
            i.putExtra("from_activity", SetBoardFragment.class.getName());
            startActivity(i);
            break;

        case R.id.menu__action_show_solutions:
            if (mPresenter == null) {
                Log.e(TAG, "mPresenter is null");
            } else {
                boolean display = !mPresenter.isDisplayAllSolutions();
                mPresenter.setDisplayAllSolutions(display);
                item.setChecked(display);
                resetSolutions();
                GAHelper.event(getActivity(), GAEvent.SetBoard_ShowSolutions, display ? "display" : "hide");//NLS
            }
            return true;

        case 42: // solve now, only useful for debugging
            if (DEBUG && mPresenter != null) {
                new AsyncTask<Void, Void, Void>() {
                    @Override
                    protected Void doInBackground(Void... voids) {
                        for (SetSolution s : mPresenter.getModel().getPossibleSolutions()) {
                            getActivity().runOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    mPresenter.deselectAllCells();
                                }
                            });
                            final SetSolution s2 = s;
                            for (int k = 0; k < SetSolution.N; k++) {
                                final int k2 = k;
                                getActivity().runOnUiThread(new Runnable() {
                                    @Override
                                    public void run() {
                                        int j = findButtonForCard(s2.mCards[k2]);
                                        if (j >= 0) {
                                            mButtons[j].setChecked(true);
                                            mPresenter.onCellChanged(j, true);
                                        }
                                    }
                                });
                                try {
                                    Thread.sleep(300);
                                } catch (InterruptedException ignore) {}
                            }
                            try {
                                Thread.sleep(500);
                            } catch (InterruptedException ignore) {}
                        }
                        return null;
                    }
                }.execute();
            }
        }
        return super.onOptionsItemSelected(item);
    }

    private void onOptionsNewBoard() {
        if (mPresenter == null) {
            Log.e(TAG, "mPresenter is null");                                                       //NLS
            return;
        }
        final SetModel model = mPresenter.getModel();

        AlertDialog.Builder b = new AlertDialog.Builder(getActivity());
        b.setTitle("New Board");
        b.setMessage(String.format("%d out of %d solutions found.\nTime: %s.\nAre you sure you want to abandon this game?",
                model.getUserSolutions().size(),
                model.getPossibleSolutions().size(),
                formatTime(mPresenter.getTimeCounterMs(), false)));
        b.setNegativeButton("No", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialogInterface, int i) {
                // Leave the activity intact.
                GAHelper.event(getActivity(), GAEvent.SetBoard_New, "cancel");               //NLS
            }
        });
        b.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialogInterface, int i) {
                mPresenter.randomizeNewBoard(new Random());
                mPresenter.deselectAllCells();
                GAHelper.event(getActivity(), GAEvent.SetBoard_New, "yes");                  //NLS
            }
        });

        b.show();
    }

    private void showOnBoardCompleted(SetResult setResult) {
        if (mPresenter == null) {
            Log.e(TAG, "mPresenter is null");                                                //NLS
            return;
        }
        mPresenter.getModel().getStats().beginSaveAsync(getActivity());
        SetCompletedDialog.showDialog(getActivity(), setResult);
    }

    @Override
    public void onCompletedDialogCancel() {
        GAHelper.event(getActivity(), GAEvent.SetBoard_New, "cancel");                       //NLS

        if (mPresenter != null) {
            // Finish the activity as-is but keep in mind we want to get a new game when we come back.
            mPresenter.getModel().setReadyForNewGame(true);
        }

        getActivity().finish();
    }

    @Override
    public void onCompletedDialogNewBoard() {
        if (mPresenter != null) {
            mPresenter.randomizeNewBoard(new Random());
            mPresenter.deselectAllCells();
        }
        GAHelper.event(getActivity(), GAEvent.SetBoard_New, "yes");                          //NLS
    }

    private void onButtonChecked(View v) {
        if (mPresenter != null && v instanceof Checkable) {
            Object i = v.getTag(R.id.cell_index);
            if (i instanceof Integer) {
                mPresenter.onCellChanged((Integer) i, ((Checkable) v).isChecked());
                // unpause time on first touch
                mTimeIsPaused = false;
            }
        }
    }

    @Override
    public void resetBoard() {
        if (mPresenter == null) { Log.e(TAG, "mPresenter is null"); return; }                       //NLS

        final SetModel model = mPresenter.getModel();
        updateCells(model.getBoard());
        updateTime();
    }

    @Override
    public void resetSolutions() {
        if (mPresenter == null) { Log.e(TAG, "mPresenter is null"); return; }                       //NLS

        final SetModel model = mPresenter.getModel();
        if (mPresenter.isDisplayAllSolutions()) {
            updateSolutions(model.getPossibleSolutions(), model.getUserSolutions());
        } else {
            updateSolutions(model.getUserSolutions(), model.getPossibleSolutions());
        }
    }

    @Override
    public void onNewUserSolution(int msg) {
        if (mPresenter == null) { Log.e(TAG, "mPresenter is null"); return; }                       //NLS

        final SetModel model = mPresenter.getModel();
        String str = null;
        switch (msg) {
        case IAbstractView.MSG_SOLUTION_NEW_SET_FOUND:
            str = String.format("New set: Found %d out of %d.",
                    model.getUserSolutions().size(), model.getPossibleSolutions().size());
            GAHelper.event(getActivity(), GAEvent.SetSolution_NewSetFound);
            break;
        case IAbstractView.MSG_SOLUTION_ALREADY_FOUND:
            str = "Valid set...  Already found.";
            GAHelper.event(getActivity(), GAEvent.SetSolution_AlreadyFound);
            break;
        case IAbstractView.MSG_SOLUTION_NOT_A_SET:
            str = "Sorry, not a valid set.";
            GAHelper.event(getActivity(), GAEvent.SetSolution_NotASet);
            break;
        }
        if (str != null) {
            Toast.makeText(getActivity(), str, Toast.LENGTH_SHORT).show();
        }
    }

    @Override
    public void onBoardCompleted(SetResult setResult) {
        GAHelper.event(getActivity(), GAEvent.SetSolution_NewSetFound);
        GAHelper.event(getActivity(), GAEvent.SetSolution_Completed,
                Integer.toString(setResult.getLevel()),
                setResult.getBoardTimeMs() / 1000 /*in sec*/);
        showOnBoardCompleted(setResult);
    }

    private void updateSolutions(@NonNull Set<SetSolution> displaySolutions,
                                 @NonNull Set<SetSolution> compareSolutions) {
        FragmentManager fm = getFragmentManager();
        assert fm != null;
        Fragment fr = fm.findFragmentById(R.id.set_solutions_fragment);
        if (fr instanceof SetSolutionsFragment) {
            ((SetSolutionsFragment)fr).updateSolutions(displaySolutions, compareSolutions);
        }
    }

    @Override
    public void deselectCells(@NonNull int... indexes) {
        int delay = 150; /* ms */
        for (int index : indexes) {
            if (index >= 0 && index < mButtons.length) {
                final SetCheckButton b = mButtons[index];
                b.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        b.setChecked(false);
                    }
                }, delay);
                delay += 150;
            }
        }
    }

    private void updateCells(@NonNull SetBoard board) {
        for (int k = 0; k < SetBoard.SXY; k++) {
            //noinspection ConstantConditions
            if (board == null) Log.d(TAG, "board == null");                                         //NLS
            if (board.getCell(k) == null) Log.d(TAG, "mGame.getCell(" + k + ") == null");           //NLS
            if (mButtons[k] == null) Log.d(TAG, "mButtons[k](" + k + ") == null");                  //NLS
            mButtons[k].setCard(board.getCell(k));
        }
    }

    private void updateTime() {
        if (mTimeCounterView == null) return;
        long ms = mPresenter == null ? 0 : mPresenter.getTimeCounterMs();
        String s = formatTime(ms, true);
        mTimeCounterView.setText(s);
    }

    private @NonNull String formatTime(long ms, boolean blink) {
        return formatTime(mTimeSB, ms, blink);
    }

    public static @NonNull String formatTime(@Null StringBuilder sb, long ms, boolean blink) {
        long s = ms / 1000;
        long m = (s / 60) % 60;
        long h = (m / 60);
        s = s % 60;

        if (sb == null) {
            sb = new StringBuilder();
        } else {
            sb.setLength(0);
        }
        if (h > 0) sb.append(h).append("h ");                                                       //NLS
        if (m < 10) sb.append('0');
        sb.append(m);
        sb.append(blink && ((ms % 1000) > 500) ? ' ' : ':');
        if (s < 10) sb.append('0');
        sb.append(s);
        return sb.toString();
    }

    private void incTime() {
        long now = SystemClock.elapsedRealtime();
        if (mTimeLastUpdateMs != 0 && mPresenter != null) {
            mPresenter.incTimeCounterMs(now - mTimeLastUpdateMs);
        }
        mTimeLastUpdateMs = now;
    }

    private boolean handleMessage(Message message) {
        switch(message.what) {
        case MSG_UPDATE_TIME_COUNTER:
            updateTime();
            mTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME_COUNTER, 500);

            if (mTimeIsPaused || mTimeWaitForWindowFocus) {
                mTimeLastUpdateMs = 0; // will reset start offset at next inc time
                if (DEBUG) Log.d(TAG, "Time paused due to: pause:" + Boolean.toString(mTimeIsPaused) +  //NLS
                        " | window:" + Boolean.toString(mTimeWaitForWindowFocus));                      //NLS
            } else {
                incTime();
            }
            return true;

        case MSG_UNPAUSE_TIME_COUNTER:
            // Setting the flag this way makes it possible to delay by 2 seconds in startTimeCounter
            mTimeIsPaused = false;
            if (DEBUG) Log.d(TAG, "MSG_UNPAUSE_TIME_COUNTER => time pause set to " + Boolean.toString(mTimeIsPaused));//NLS
            return true;
        }
        return false;
    }

    private void startTimeCounter() {
        mTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME_COUNTER, 500);     // 1/2 sec
        mTimeHandler.sendEmptyMessageDelayed(MSG_UNPAUSE_TIME_COUNTER, 2000);   // 2 sec
    }

    private void stopTimeCounter() {
        mTimeHandler.removeMessages(MSG_UPDATE_TIME_COUNTER);
        mTimeHandler.removeMessages(MSG_UNPAUSE_TIME_COUNTER);
        mTimeIsPaused = true;
    }

    /** Invoked by SetActivity.onWindowFocusChanged */
    public void onWindowFocusChanged(boolean hasFocus) {
        mTimeWaitForWindowFocus = !hasFocus;
        if (DEBUG) Log.d(TAG, "onWindowFocusChanged => time pause set to " + Boolean.toString(mTimeWaitForWindowFocus)); //NLS
    }

    private int findButtonForCard(SetCard c) {
        for (int i = 0; i < SetBoard.SXY; i++) {
            SetCheckButton b = mButtons[i];
            if (b != null && c.equals(b.getCard())) {
                return i;
            }
        }
        return -1;
    }
}
